package com.zb.zeus.common.engine;

import com.google.common.collect.Range;
import com.zb.zeus.common.core.exception.BaseException;
import com.zb.zeus.common.engine.annotation.FormulaDesc;
import com.zb.zeus.common.engine.domain.CalcConst;
import com.zb.zeus.common.engine.domain.DurUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.util.Assert;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 计算公式
 *
 * @author fujianjian
 * @since 2021/4/7 10:35
 */
@Slf4j
public class Formula {

    @FormulaDesc(desc = "if判断公式实现 使用如：iif(value(\"统一单价\")<10,10,5)")
    public static Object iif(boolean flag, Object ret1, Object ret2) {
        if (flag) {
            return  ret1 instanceof String ? Calculator.executeFormula((String) ret1) : ret1;
        } else {
            return ret2 instanceof String ? Calculator.executeFormula((String) ret2) : ret2;
        }
    }

    @FormulaDesc(desc = "perCount单一计数公式 使用如：perCount(10, -2)=安全事故10起,每起事故-2分，总共-20")
    public static BigDecimal perCount(BigDecimal actVal, BigDecimal per) {
        BigDecimal num = actVal.multiply(per).setScale(2, RoundingMode.HALF_UP);
        log.info("perCount({}, {}) = [{}]", actVal, per, num);
        return num;
    }

    @FormulaDesc(desc = "abs绝对值公式实现 使用如：abs(-100)")
    public static BigDecimal abs(Object obj) {
        BigDecimal testVal = Calculator.obj2BigDecimal(obj);
        BigDecimal num =  BigDecimal.valueOf(Math.abs(testVal.doubleValue()));
        log.info("abs({}) = [{}]", obj, num);
        return num;
    }

    @FormulaDesc(desc = "自定义取整公式 后两个参数没有默认四舍五入保留两位 " +
            "第二个参数：保留小数位数 第三个参数：0-向上取整 1-向下取整 3-四舍五入取整 " +
            "使用如：a、round(-100.256,2,3)  b、round(-100.256)")
    public static BigDecimal round(Object obj, int... scaleMode) {
        BigDecimal testVal = Calculator.obj2BigDecimal(obj);
        if (Objects.isNull(scaleMode) || scaleMode.length == 0) {
            scaleMode = new int[]{CalcConst.TWO, BigDecimal.ROUND_HALF_UP};
        } else if (scaleMode.length < CalcConst.TWO) {
            int scale = scaleMode[0];
            scaleMode = new int[]{scale, BigDecimal.ROUND_HALF_UP};
        }
        // 保留小数位数
        int scale = scaleMode[0];
        // 向上取整模式
        int mode = scaleMode[1];
        return testVal.setScale(scale, mode == BigDecimal.ROUND_UP || mode == BigDecimal.ROUND_DOWN ? mode : BigDecimal.ROUND_HALF_UP);
    }

    @FormulaDesc(desc = "divide除法运算公式, 第一个参数：被除数，第二个参数：除数，第三个参数：保留小数位数 第四个参数：0-向上取整 1-向下取整 3-四舍五入取整" +
            "使用如：divide(1,3)=0.33; divide(1,3,3,0)=0.334, divide(1,3,5)=0.33333")
    public static BigDecimal divide(BigDecimal dividend, BigDecimal divisor, int... scaleMode) {
        if (Objects.isNull(dividend) || Objects.isNull(divisor)) {
            return BigDecimal.ZERO;
        }
        if (divisor.compareTo(BigDecimal.ZERO) == 0) {
            return BigDecimal.ZERO;
        }
        int scale = 2, mode = BigDecimal.ROUND_HALF_UP;
        if (ArrayUtils.isNotEmpty(scaleMode)) {
            if (scaleMode.length > 0) {
                scale = scaleMode[0];
            }
            if (scaleMode.length > CalcConst.ONE) {
                int paramMode = scaleMode[1];
                mode = paramMode == BigDecimal.ROUND_UP || paramMode == BigDecimal.ROUND_DOWN ? paramMode : mode;
            }
        }
        BigDecimal num = dividend.divide(divisor, scale, mode);
        log.info("divide({}, {}, {}) = [{}]", dividend, divisor, Arrays.toString(scaleMode), num);
        return num;
    }


    /**
     * region 公式
     *
     * @param testValue     实际值
     * @param regionExpress 表达式
     * @return {@linkplain BigDecimal}
     */
    @FormulaDesc(desc = "区间公式 使用如：region(190,\"1.5=[100,200];2.0=(201,300]\")")
    public static BigDecimal region(BigDecimal testValue, String regionExpress) {
        String cleanRegion = regionExpress.replace(StringUtils.SPACE, StringUtils.EMPTY);
        List<Region.RangeEntry<BigDecimal>> entries = Region.getRangeEntry(cleanRegion);
        BigDecimal valDecimal = testValue;
        for (Region.RangeEntry<BigDecimal> entry : entries) {
            Range<BigDecimal> range = entry.getIndexRange(0);
            if (range.contains(valDecimal)) {
                log.info(String.format("region(%s,%s) = [%s]", testValue, regionExpress, entry.getVal()));
                return entry.getVal();
            }
        }
        return BigDecimal.ZERO;
        //return Calculator.preseExpress(testValue, regionExpress);
    }

    /**
     * 求和公式，和税务一样
     * eg: sectionSum(12000, "0.1=(0, 3000],0.2=(3000, 6000],0.5=(6000, 10000],0.65=(10000, 15000]") = 4200
     *
     * @param val    实际值
     * @param region 区间
     * @return
     */
    @FormulaDesc(desc = "求和公式，和税务一样 使用如：sectionSum(12000, \"0.1=(0, 3000];0.2=(3000, 6000];0.5=(6000, 10000];0.65=(10000, 15000]\")")
    public static BigDecimal sectionSum(Object val, String region) {
        BigDecimal testVal = Calculator.obj2BigDecimal(val);
        BigDecimal sumVal = BigDecimal.ZERO;
        BigDecimal posInfinity = BigDecimal.valueOf(Integer.MAX_VALUE);
        BigDecimal negInfinity = BigDecimal.valueOf(Integer.MIN_VALUE);
        String cleanRegion = region.replace(StringUtils.SPACE, StringUtils.EMPTY);
        List<Region.RangeEntry<BigDecimal>> entries = Region.getRangeEntry(cleanRegion);
        Optional<Region.RangeEntry<BigDecimal>> findOptional = entries.parallelStream().filter(re -> re.getIndexRange(0).contains(testVal)).findFirst();
        if (!findOptional.isPresent()) {
            return BigDecimal.ZERO;
        }
        boolean useLowVal = findOptional.get().getVal().compareTo(BigDecimal.ZERO) >= 0;
        for (Region.RangeEntry<BigDecimal> entry : entries) {
            Range<BigDecimal> range = entry.getIndexRange(0);
            BigDecimal upVal = range.hasUpperBound() ? range.upperEndpoint() : posInfinity;
            BigDecimal lowVal = range.hasLowerBound() ? range.lowerEndpoint() : negInfinity;
            BigDecimal rangeVal = entry.getVal();
            if (useLowVal) {
                if (range.contains(testVal)) {
                    sumVal = sumVal.add(testVal.subtract(lowVal).multiply(rangeVal));
                    break;
                } else if (testVal.compareTo(upVal) > -1 && rangeVal.compareTo(BigDecimal.ZERO) >= 0) {
                    BigDecimal regionVal = upVal.subtract(lowVal);
                    sumVal = sumVal.add(regionVal.multiply(rangeVal));
                }
            } else {
                if (range.contains(testVal)) {
                    sumVal = sumVal.add(upVal.subtract(testVal).multiply(rangeVal));
                } else if (testVal.compareTo(lowVal) < 0 && rangeVal.compareTo(BigDecimal.ZERO) < 0) {
                    BigDecimal regionVal = upVal.subtract(lowVal);
                    sumVal = sumVal.add(regionVal.multiply(rangeVal));
                }
            }
        }
        log.info(String.format("sectionSum(%s,%s) = [%s]", val, region, sumVal));
        return sumVal.setScale(4, BigDecimal.ROUND_HALF_UP);
    }





    /**
     * @param desc
     * @return
     */
    /**
     * 多维区间计算
     * eg: multiRegions("(3,4)", "2=(1,3),(1,2]; 10=[3,6),(2, 20]; 100=[6,10),(20,30]") = 10
     *
     * @param calcVal 计算值
     * @param regions 区间值
     * @return
     */
    @FormulaDesc(desc = "多维区间计算 使用如：multiRegions(\"(3,4)\", \"2=(1,3),(1,2]; 10=[3,6),(2, 20]; 100=[6,10),(20,30]\")")
    public static BigDecimal multiRegions(String calcVal, String regions) {
        String cleanCalcVal = calcVal.replace(StringUtils.SPACE, StringUtils.EMPTY)
                .replaceAll("\\(|\\)", StringUtils.EMPTY);
        String[] valArrStr = cleanCalcVal.split(CalcConst.COMMA);
        String cleanRegions = regions.replace(StringUtils.SPACE, StringUtils.EMPTY);
        try {
            List<Region.RangeEntry<BigDecimal>> entries = Region.getRangeEntry(cleanRegions);
            for (Region.RangeEntry<BigDecimal> entry : entries) {
                boolean hasHit = true;
                for (int i = 0; i < valArrStr.length; i++) {
                    BigDecimal valDecimal = NumberUtils.createBigDecimal(valArrStr[i]);
                    if (!entry.getIndexRange(i).contains(valDecimal)) {
                        hasHit = false;
                        break;
                    }
                }
                if (hasHit) {
                    log.info(String.format("multiRegions(%s,%s) = [%s]", calcVal, regions, entry.getVal()));
                    return entry.getVal();
                }
            }
        } catch (Exception e) {
            throw new BaseException("公式书写有误计算失败", e);
        }
        return BigDecimal.ZERO;
    }

    @FormulaDesc(desc = "获取参数中最小的 使用如：min(2,4,5)")
    public static BigDecimal min(Object... nums) {
        List<BigDecimal> valList = Calculator.objNumArray2List(nums);
        BigDecimal min = valList.stream().min(Comparator.naturalOrder()).orElse(BigDecimal.ZERO);
        log.info(String.format("min(%s) = [%s]", Arrays.stream(nums).map(Object::toString).collect(Collectors.joining(",")), min));
        return min;
    }

    @FormulaDesc(desc = "获取参数中最大的 使用如：max(2,4,5)")
    public static BigDecimal max(Object... nums) {
        List<BigDecimal> valList = Calculator.objNumArray2List(nums);
        BigDecimal max = valList.stream().max(Comparator.naturalOrder()).orElse(BigDecimal.ZERO);
        log.info(String.format("max(%s) = [%s]", Arrays.stream(nums).map(Object::toString).collect(Collectors.joining(",")), max));
        return max;
    }

    @FormulaDesc(desc = "区间差值计算 使用如：regionDeltaVal(8, \"-1000=(,6.5);0=[6.5,7];2000=(7,)\", 1000)=min(abs((8-7)*200),1000)=1000")
    public static BigDecimal regionDeltaVal(Object val, String region, Object threshold) {
        BigDecimal testVal = Calculator.obj2BigDecimal(val);
        if (testVal.compareTo(BigDecimal.ZERO) == 0) {
            return testVal;
        }
        String cleanRegion = region.replace(StringUtils.SPACE, StringUtils.EMPTY);
        List<Region.RangeEntry<BigDecimal>> entries = Region.getRangeEntry(cleanRegion);
        BigDecimal targetVal = BigDecimal.ZERO;
        for (Region.RangeEntry<BigDecimal> entry : entries) {
            Range<BigDecimal> range = entry.getIndexRange(0);
            if (range.contains(testVal)) {
                int valCompared = entry.getVal().compareTo(BigDecimal.ZERO);
                int stepCompared = entry.getStep().compareTo(BigDecimal.ZERO);
                if (valCompared < 0) {
                    targetVal = stepCompared == 0 ?
                            range.upperEndpoint().subtract(testVal).multiply(entry.getVal()) :
                            range.upperEndpoint().subtract(testVal)
                                    .divide(entry.getStep(), 0, RoundingMode.DOWN)
                                    .multiply(entry.getVal());
                    targetVal = min(threshold, targetVal.negate()).negate();
                    break;
                } else if (valCompared == 0) {
                    targetVal = BigDecimal.ZERO;
                    break;
                } else {
                    targetVal = stepCompared == 0 ?
                            testVal.subtract(range.lowerEndpoint()).multiply(entry.getVal()) :
                            testVal.subtract(range.lowerEndpoint())
                                    .divide(entry.getStep(),0,RoundingMode.DOWN)
                                    .multiply(entry.getVal());
                    targetVal = min(threshold, targetVal);
                    break;
                }
            }
        }
        BigDecimal ret = targetVal.setScale(2, RoundingMode.HALF_UP);
        log.info(String.format("regionDeltaVal(%s,%s,%s) = [%s]", val, region, threshold, ret));
        return ret;
    }

    @FormulaDesc(desc = "日期区间计算,默认单位为天,最后一个参数可以指定区间单位; year-年 month-月 day-天 hour-小时 min-分钟 sec-秒," +
            " 使用如：getDateDuration(actual, target)=2 或getDateDuration(actual, target, 'min')=2")
    public static BigDecimal getDateDuration(BigDecimal start, BigDecimal end, String... unit) {
        Assert.notNull(start, "区间计算开始时间不能为空");
        Assert.notNull(start, "区间计算结束时间不能为空");
        DurUnit durUnit = ArrayUtils.isEmpty(unit) ? DurUnit.DAY : DurUnit.getInstance(unit[0]);
        long duration = durUnit.getDuration(start.longValue(), end.longValue());
        return BigDecimal.valueOf(duration);
    }

}
